package com.hzw.saas.web.sso.service;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.hzw.saas.common.config.exception.DefaultSaasException;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.config.util.RedisUtil;
import com.hzw.saas.web.sso.controller.dto.SsoTokenDto;
import com.hzw.saas.web.sso.exception.RedirectErrorException;
import com.hzw.saas.web.sso.exception.SaasOauthException;
import com.hzw.saas.web.sso.exception.ScopeErrorException;
import com.hzw.saas.web.sso.exception.UsernamePasswordErrorException;
import com.hzw.saas.web.sso.pojo.dto.CodeInfoDto;
import com.hzw.saas.web.sso.pojo.dto.TokenInfoDto;
import com.hzw.saas.web.sso.pojo.model.SaasClient;
import com.hzw.saas.web.sso.pojo.redis.SsoUser;
import com.hzw.saas.web.sso.security.CustomAdditionalInformation;
import com.hzw.saas.web.sso.util.OauthUtils;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.hzw.saas.web.sso.util.Constants.*;

/**
 * @author sonam
 * @sine 2021/11/9 5:26 下午
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SaasOauthServiceImpl implements ISaasOauthService{

    public static final String USER_TOKEN_KEY = "SSO:TOKEN:";

    private final PasswordEncoder passwordEncoder;
    private final UserDetailsService userDetailsService;
    private final AuthorizationServerTokenServices userTokenServices;

    @Override
    public void check(SaasClient client, String redirectUri, List<String> scopes, String responseType) throws SaasOauthException {
        redirectUri = URLUtil.decode(redirectUri);
        // 校验接收参数与客户端是否匹配
        AssertUtil.assertThrow("参数错误", Objects.isNull(client));
        if(!OauthUtils.checkRedirectUri(Arrays.stream(client.getRedirectUri().split(",")).collect(Collectors.toList()), redirectUri)) {
            throw new RedirectErrorException("redirect_uri参数错误");
        }
        if(!Objects.equals(responseType, "code")) {
            throw new RedirectErrorException("授权参数错误");
        }
        if (CollectionUtil.isNotEmpty(scopes)) {
            for (String scope : scopes) {
                if (!client.getScope().contains(scope)) {
                    throw new ScopeErrorException("scope参数错误");
                }
            }
        }
    }

    @Override
    public CodeInfoDto getCode(SaasClient client, List<String> scopes, String redirectUri, String username, String password) throws SaasOauthException {
        this.check(client, redirectUri, scopes, "code");
        redirectUri = URLUtil.decode(redirectUri);
        String[] urlStrings = redirectUri.split("\\?");
        redirectUri = urlStrings[0];
        String paramStr = StrUtil.EMPTY;
        if (urlStrings.length > 1) {
            paramStr = urlStrings[1];
        }

        String clientId = client.getId();
        OAuth2AccessToken oAuth2AccessToken = doLogin(client, scopes, username, password);
        // 生成code 5分钟内有效
        String code = IdUtil.nanoId(12);
        // 授权码与accessToken关联
        CodeInfoDto codeInfo = new CodeInfoDto();
        codeInfo.setCode(code);
        codeInfo.setClientId(clientId);
        codeInfo.setUsername(username);
        if(StrUtil.isBlank(paramStr)) {
            codeInfo.setRedirectUrl(StrUtil.format("{}?code={}", redirectUri, code));
        } else {
            codeInfo.setRedirectUrl(StrUtil.format("{}?{}&code={}", redirectUri, paramStr, code));
        }
        // 生成的token
        codeInfo.setOAuth2AccessToken(oAuth2AccessToken);
        // 设置code与access_token，5min有效
        OauthUtils.setCodeToken(clientId, code, oAuth2AccessToken);
        return codeInfo;
    }

    @Override
    public TokenInfoDto getTokenByCode(SaasClient saasClient, String clientSecret, String code) throws SaasOauthException {
        return token(AUTHORIZATION_CODE, null, null, saasClient, clientSecret, code, null);
    }

    @Override
    public TokenInfoDto refreshToken(SaasClient saasClient, String refreshToken) throws SaasOauthException{
        return token(REFRESH_TOKEN, null, null, saasClient, null, null, refreshToken);
    }

    @Override
    public TokenInfoDto password(SaasClient saasClient, String clientSecret, String username, String password) throws SaasOauthException {
        return token(PASSWORD, username, password, saasClient, clientSecret, null, null);
    }

    /**
     * 获取access_token令牌
     * 该接口有3种模式，
     * 1、授权码模式 /saas/oauth2/access_token?client_id=&redirect_uri=&code="
     * 2、密码模式 /saas/oauth2/password?client_id=&username=&password=
     * 3、刷新token模式 /saas/oauth2/refresh_token?client_id=&refresh_token=
     */
    protected TokenInfoDto token(@ApiParam(value = "授权类型,（授权码模式 authorization_code | 密码模式 password | 刷新token（refresh_token））", defaultValue = "password")
                                 @RequestParam(defaultValue = "password") String grant_type,
                                 @ApiParam(value = "用户名(密码模式需要)") @RequestParam(required = false) String username,
                                 @ApiParam(value = "用户密码(密码模式需要)") @RequestParam(required = false) String password,
                                 @ApiParam(value = "客户端id", required = true) @RequestParam SaasClient client,
                                 @ApiParam(value = "客户端秘钥(密码模式不需要)") @RequestParam(required = false) String client_secret,
                                 @ApiParam(value = "认证返回的code(授权模式需要)") @RequestParam(required = false) String code,
                                 @ApiParam(value = "刷新token(刷新token模式需要)") @RequestParam(required = false) String refresh_token) throws SaasOauthException {
        AssertUtil.assertThrow(StrUtil.format("不支持grant type: {}", grant_type), !client.getAuthorizedGrantTypes().contains(grant_type));
        OAuth2AccessToken oAuth2AccessToken;
        String clientId = client.getId();
        if (Objects.equals(AUTHORIZATION_CODE, grant_type)) {
            AssertUtil.assertThrow("client_secret不正确", !Objects.equals(client.getClientSecret(), client_secret));
            // 通过授权码获取 token
            oAuth2AccessToken = OauthUtils.getCodeToken(clientId, code);
            AssertUtil.assertThrow("授权码已过期", Objects.isNull(oAuth2AccessToken));
            OauthUtils.delCodeToken(clientId, code);
        } else if (Objects.equals(REFRESH_TOKEN, grant_type)) {
            // 刷新token
            TokenRequest tokenRequest = new TokenRequest(null, clientId, null, null);
            oAuth2AccessToken = userTokenServices.refreshAccessToken(refresh_token, tokenRequest);
            AssertUtil.assertThrow("服务器内部错误，刷新access_token失败", HttpStatus.INTERNAL_SERVER_ERROR, Objects.isNull(oAuth2AccessToken));
        } else if (Objects.equals(PASSWORD, grant_type)) {
            AssertUtil.assertThrow("client_secret不正确", !Objects.equals(client.getClientSecret(), client_secret));
            // 密码模式
            oAuth2AccessToken = doLogin(client, null, username, password);
        } else {
            throw DefaultSaasException.build("授权类型grant_type不正确").status(HttpStatus.BAD_REQUEST);
        }

        AssertUtil.assertThrow("服务器内部错误，无法获取access_token", HttpStatus.INTERNAL_SERVER_ERROR, Objects.isNull(oAuth2AccessToken));

        TokenInfoDto tokenInfo = new TokenInfoDto();
        tokenInfo.setAccessToken(oAuth2AccessToken.getValue());
        tokenInfo.setExpiresIn(oAuth2AccessToken.getExpiresIn());
        Optional<OAuth2RefreshToken> refreshToken = Optional.ofNullable(oAuth2AccessToken.getRefreshToken());
        tokenInfo.setRefreshToken(refreshToken.map(OAuth2RefreshToken::getValue).orElse(StrUtil.EMPTY));
        tokenInfo.setUserId(oAuth2AccessToken.getAdditionalInformation().get(CustomAdditionalInformation.USER_ID).toString());
        return tokenInfo;
    }

    // 假装登录
    private OAuth2AccessToken doLogin(SaasClient saasClient, List<String> scopes, String username, String password) throws UsernamePasswordErrorException {
        // 登录认证（模拟登录）
        UserDetails user;
        try {
            user = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException e) {
            log.error("用户名找不到", e);
            throw new UsernamePasswordErrorException("用户或密码不正确");
        }
        // 微信登录是不需要密码的所以这里要比对加密之后的字符串
        if(!passwordEncoder.matches(password, user.getPassword()) && !Objects.equals(password, user.getPassword())) {
            throw new UsernamePasswordErrorException("用户或密码不正确");
        }
        // 创建accessToken
        Object credentials = user.getPassword();
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, credentials, user.getAuthorities());
        TokenRequest tokenRequest = new TokenRequest(null, saasClient.getId(),
            CollectionUtil.isEmpty(scopes) ? Arrays.stream(saasClient.getScope().split(StrUtil.COMMA)).collect(Collectors.toSet()) : scopes, null);
        BaseClientDetails client = new BaseClientDetails();
        client.setClientId(saasClient.getId());
        client.setResourceIds(Arrays.stream(saasClient.getResourceIds().split(StrUtil.COMMA)).collect(Collectors.toSet()));
        return userTokenServices.createAccessToken(new OAuth2Authentication(tokenRequest.createOAuth2Request(client), authentication));
    }


    @Override
    public SsoTokenDto buildSsoToken(UserDetails user, String password) {
        SsoUser ssoUser = new SsoUser();
        ssoUser.setUserDetails(user);
        ssoUser.setPassword(password);
        String token = IdUtil.simpleUUID();
        // SSO登录页面12小时登录状态
        RedisUtil.set(USER_TOKEN_KEY.concat(token), ssoUser, 60 * 60 * 12);
        SsoTokenDto tokenDto = new SsoTokenDto();
        tokenDto.setSsoToken(token);
        return tokenDto;
    }

    @Override
    public SsoUser getSsoTokenInfo(String ssoToken) {
        Object user = RedisUtil.get(USER_TOKEN_KEY.concat(ssoToken));
        return user instanceof SsoUser ? (SsoUser) user : null;
    }
}
